package mvc;

import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import renderer.JavaScriptRenderer;
import renderer.Renderer;
import renderer.TextRenderer;
import template.JspTemplateFactory;
import template.TemplateFactory;
import container.ContainerFactory;
import converter.ConverterFactory;

/**
 * Dispatcher handles ALL requests from clients, and dispatches to appropriate 
 * handler to handle each request.
 * 
 * @author Michael Liao (askxuefeng@gmail.com)
 */
class Dispatcher {
    private final Log log = LogFactory.getLog(getClass());

    private ServletContext 			servletContext;
    private ContainerFactory 		containerFactory;
    private boolean 				multipartSupport 	= false;
    private long 					maxFileSize 		= 10L * 1024L * 1024L; // default to 10M.
    private Interceptor[] 			interceptors 		= null;
    private ExceptionHandler 		exceptionHandler 	= null;
    
    private UrlMatcher[] 			getMatchers 		= null;
    private UrlMatcher[] 			postMatchers 		= null;
    private Map<UrlMatcher, Action> getUrlMap 			= new HashMap<UrlMatcher, Action>();
    private Map<UrlMatcher, Action> postUrlMap 			= new HashMap<UrlMatcher, Action>();
    
    private ConverterFactory 		converterFactory 	= new ConverterFactory();

    public void init(Config config) throws ServletException {
        log.info("Init Dispatcher...");
        this.servletContext = config.getServletContext();
        try {
            initAll(config);
        } catch (ServletException e) {
            throw e;
        } catch (Exception e) {
            throw new ServletException("Dispatcher init failed.", e);
        }
    }
    
    public boolean service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    	String url = req.getRequestURI();
    	String path = req.getContextPath();
    	if (path.length()>0) {
    		url = url.substring(path.length());
    	}
    	// set default character encoding to "utf-8" if encoding is not set:
    	if (req.getCharacterEncoding()==null) {
    		req.setCharacterEncoding("UTF-8");
    	}
    	if (log.isDebugEnabled()) {
    		log.debug("Handle for URL: " + url);
    	}
    	
    	
    	String reqMethod = req.getMethod().toUpperCase();
    	Object[] arguments = null;
    	Action action = null;
    	String[] args;
    	if(reqMethod == "GET"){
    		for (UrlMatcher matcher : this.getMatchers) {
        		args = matcher.getUrlParameters(url);
        		if (args!=null) {
        			action = getUrlMap.get(matcher);
        			arguments = getArgument(req ,action ,matcher ,args);
        			break;
        		}
        	}
    	} else if ( reqMethod == "POST"){
    		for (UrlMatcher matcher : this.postMatchers) {
        		args = matcher.getUrlParameters(url);
        		if (args!=null) {
        			action = getUrlMap.get(matcher);
        			arguments = getArgument(req ,action ,matcher ,args);
        			break;
        		}
        	}
    	}
    	
    	Execution execution = null;
    	if (action!=null) {
			execution = new Execution(req, resp, action, arguments);
			handleExecution(execution, req, resp);
		}
    	
    	return execution!=null;
    }

    /**
     * @param req
     * @param action
     * @param matcher
     */
    Object[] getArgument(HttpServletRequest req ,Action action ,UrlMatcher matcher ,String[] args) {
    	Object[] arguments = new Object[action.arguments.length];
		
		/*get argument from url*/
    	Class<?> type;
		int argIndex;
		int[] urlArgOrder = matcher.orders;
		for (int i=0; i<args.length; i++) {
			argIndex = urlArgOrder[i];
			type = action.arguments[argIndex];
			arguments[argIndex] = converterFactory.convert(type, args[i]);
		}
		
		/*get argument from request*/
		Map<String ,String[]> paramMap = req.getParameterMap();
		Object instance;
		Map<String ,ParameterInjector> injectors = action.injectors;
		ParameterInjector injector;
		for(String key : paramMap.keySet()){
			injector = injectors.get(key);
			if(injector != null){
				if(injector.needInject){
					instance = arguments[injector.index];
					if(instance == null){
						try {
							/*create a instance the first time you use*/
							instance = injector.argumentType.newInstance();
							arguments[injector.index] = instance;
						} catch (InstantiationException e) {
							e.printStackTrace();
						} catch (IllegalAccessException e) {
							e.printStackTrace();
						}
					}
					
					injector.inject(instance, paramMap.get(key)[0], key);
				} else {
					if(!injector.isArray){
						arguments[injector.index] = converterFactory.convert(injector.argumentType, paramMap.get(key)[0]);
					} else {
						/*
						 * support array 
						 */
						String array[] = paramMap.get(key);
						int len = array.length;
						Class<?> t = injector.argumentType.getComponentType();
						Object arr = Array.newInstance(t , len);
						for(int i=0; i<len; i++){
							Array.set(arr, i, converterFactory.convert(t,array[i]));
						}
						
						arguments[injector.index] = arr;
					}
				}
			}
		}
		
    	return arguments;
    }
    
    void initAll(Config config) throws Exception {
        // detect multipart support:
        try {
            Class.forName("org.apache.commons.fileupload.servlet.ServletFileUpload");
            this.multipartSupport = true;
            log.info("Using CommonsFileUpload to handle multipart http request.");
            String maxFileSize = config.getInitParameter("maxFileSize");
            if (maxFileSize!=null) {
                try {
                    long n = Long.parseLong(maxFileSize);
                    if (n<=0)
                        throw new NumberFormatException();
                    this.maxFileSize = n;
                }
                catch (NumberFormatException e) {
                    log.warn("Invalid parameter <maxFileSize> value '" + maxFileSize + "', using default.");
                }
            }
        }
        catch (ClassNotFoundException e) {
            log.info("CommonsFileUpload not found. Multipart http request can not be handled.");
        }

        // init IoC container:
        String containerName = config.getInitParameter("container");
        if (containerName ==null) {
            throw new ConfigException("Missing init parameter <container>.");
        }
        this.containerFactory = Utils.createContainerFactory(containerName);
        this.containerFactory.init(config);
        List<Object> beans = this.containerFactory.findAllBeans();
        initComponents(beans);

        // init template engine:
        initTemplateFactory(config);
    }

    void initTemplateFactory(Config config) {
        String name = config.getInitParameter("template");
        if (name==null) {
            name = JspTemplateFactory.class.getName();
            log.info("No template factory specified. Default to '" + name + "'.");
        }
        TemplateFactory tf = Utils.createTemplateFactory(name);
        tf.init(config);
        log.info("Template factory '" + tf.getClass().getName() + "' init ok.");
        TemplateFactory.setTemplateFactory(tf);
    }

    void initComponents(List<Object> beans) {
        List<Interceptor> intList = new ArrayList<Interceptor>();
        for (Object bean : beans) {
            if (bean instanceof Interceptor) {
                intList.add((Interceptor)bean);
            }
            if (this.exceptionHandler==null && bean instanceof ExceptionHandler) {
                this.exceptionHandler = (ExceptionHandler) bean;
            }
        }
        
        log.info("##################### start matching url ...");
        ActionDetector ad = new ActionDetector();
        ad.awareActionFromBeans(beans);
        getUrlMap.putAll(ad.getGetUrlMap());
        postUrlMap.putAll(ad.getPostUrlMap());
        
        System.out.println(getUrlMap.size());
        System.out.println(postUrlMap.size());
        log.info("##################### end matching url");
        
        if (this.exceptionHandler==null) {
            this.exceptionHandler = new DefaultExceptionHandler();
        }
        this.interceptors = intList.toArray(new Interceptor[intList.size()]);
        // sort interceptors by its annotation of 'InterceptorOrder':
        Arrays.sort(
            this.interceptors,
            new Comparator<Interceptor>() {
                public int compare(Interceptor i1, Interceptor i2) {
                    InterceptorOrder o1 = i1.getClass().getAnnotation(InterceptorOrder.class);
                    InterceptorOrder o2 = i2.getClass().getAnnotation(InterceptorOrder.class);
                    int n1 = o1==null ? Integer.MAX_VALUE : o1.value();
                    int n2 = o2==null ? Integer.MAX_VALUE : o2.value();
                    if (n1==n2) {
                        return i1.getClass().getName().compareTo(i2.getClass().getName());
                    }
                    return n1<n2 ? (-1) : 1;
                }
            }
        );
        // sort url matchers by its url:
        this.getMatchers = getUrlMap.keySet().toArray(new UrlMatcher[getUrlMap.size()]);
        Arrays.sort(
        	this.getMatchers,
            new Comparator<UrlMatcher>() {
                public int compare(UrlMatcher o1, UrlMatcher o2) {
                    String u1 = o1.url;
                    String u2 = o2.url;
                    int n = u1.compareTo(u2);
                    if (n==0) {
                        throw new ConfigException("Cannot mapping one url '" + u1 + "' to more than one action method.");
                    }
                    return n;
                }
            }
        );
        
        // sort url matchers by its url:
        this.postMatchers = postUrlMap.keySet().toArray(new UrlMatcher[postUrlMap.size()]);
        Arrays.sort(
        	this.postMatchers,
            new Comparator<UrlMatcher>() {
                public int compare(UrlMatcher o1, UrlMatcher o2) {
                    String u1 = o1.url;
                    String u2 = o2.url;
                    int n = u1.compareTo(u2);
                    if (n==0) {
                        throw new ConfigException("Cannot mapping one url '" + u1 + "' to more than one action method.");
                    }
                    return n;
                }
            }
        );
    }

    // log warning message of invalid action method:
    void warnInvalidActionMethod(Method m, String string) {
        log.warn("Invalid Action method '" + m.toGenericString() + "': " + string);
    }


    void handleExecution(Execution execution, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        if (this.multipartSupport) {
            if (MultipartHttpServletRequest.isMultipartRequest(request)) {
                request = new MultipartHttpServletRequest(request, maxFileSize);
            }
        }
        ActionContext.setActionContext(servletContext, request, response);
        try {
            InterceptorChainImpl chains = new InterceptorChainImpl(interceptors);
            chains.doInterceptor(execution);
            handleResult(request, response, chains.getResult());
        }
        catch (Exception e) {
            handleException(request, response, e);
        }
        finally {
            ActionContext.removeActionContext();
        }
    }

    void handleException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws ServletException, IOException {
        try {
            exceptionHandler.handle(request, response, ex);
        }
        catch (ServletException e) {
            throw e;
        }
        catch (IOException e) {
            throw e;
        }
        catch (Exception e) {
            throw new ServletException(e);
        }
    }

    void handleResult(HttpServletRequest request, HttpServletResponse response, Object result) throws Exception {
        if (result==null)
            return;
        if (result instanceof Renderer) {
            Renderer r = (Renderer) result;
            r.render(this.servletContext, request, response);
            return;
        }
        if (result instanceof String) {
            String s = (String) result;
            if (s.startsWith("redirect:")) {
                response.sendRedirect(s.substring("redirect:".length()));
                return;
            }
            if (s.startsWith("script:")) {
                String script = s.substring("script:".length());
                new JavaScriptRenderer(script).render(servletContext, request, response);
                return;
            }
            new TextRenderer(s).render(servletContext, request, response);
            return;
        }
        throw new ServletException("Cannot handle result with type '" + result.getClass().getName() + "'.");
    }

    public void destroy() {
        log.info("Destroy Dispatcher...");
        this.containerFactory.destroy();
    }
}
